﻿using pk3DS.Core;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using pk3DS.WinForms.Properties;

namespace pk3DS.WinForms;

public static class WinFormsUtil
{
    // Image Layering/Blending Utility
    public static Bitmap LayerImage(Image baseLayer, Image overLayer, int x, int y, double trans)
    {
        if (baseLayer == null)
            return overLayer as Bitmap;
        var img = new Bitmap(baseLayer.Width, baseLayer.Height);
        using var gr = Graphics.FromImage(img);
        gr.DrawImage(baseLayer, new Point(0, 0));
        var o = ChangeOpacity(overLayer, trans);
        gr.DrawImage(o, new Rectangle(x, y, overLayer.Width, overLayer.Height));
        return img;
    }

    public static Bitmap ChangeOpacity(Image img, double trans)
    {
        if (img == null)
            return null;
        if (img.PixelFormat.HasFlag(PixelFormat.Indexed))
            return (Bitmap)img;

        var bmp = (Bitmap)img.Clone();
        var bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
        var ptr = bmpData.Scan0;

        int len = bmp.Width * bmp.Height * 4;
        byte[] data = new byte[len];

        Marshal.Copy(ptr, data, 0, len);

        for (int i = 0; i < data.Length; i += 4)
            data[i + 3] = (byte)(data[i + 3] * trans);

        Marshal.Copy(data, 0, ptr, len);
        bmp.UnlockBits(bmpData);

        return bmp;
    }

    private static ReadOnlySpan<int> GenderedSpecies => [592, 593, 521, 668];
    private static ReadOnlySpan<int> DefaultSprites => [778, 664, 665, 414, 493, 773];

    public static string GetResourceStringSprite(int species, int form, int gender, int generation)
    {
        if (DefaultSprites.Contains(species)) // Species who show their default sprite regardless of Form
            form = 0;

        string file = "_" + species;
        if (form > 0) // Alt Form Handling
            file += "_" + form;
        else if (gender == 1 && GenderedSpecies.Contains(species)) // Frillish & Jellicent, Unfezant & Pyroar
            file += "_" + gender;

        if (species == 25 && form > 0 && generation >= 7) // Pikachu
            file += "c"; // Cap

        return file;
    }

    public static Bitmap GetSprite(int species, int form, int gender, int item, GameConfig config, bool shiny = false)
    {
        if (species == 0)
            return Resources._0;
        if (species > config.MaxSpeciesID)
            return Resources.unknown;

        var file = GetResourceStringSprite(species, form, gender, config.Generation);

        // Redrawing logic
        // Redrawing logic
        Bitmap baseImage = (Bitmap)Resources.ResourceManager.GetObject(file);
        if (IsTotemForm(species, form))
        {
            form = GetTotemBaseForm(species, form);
            file = GetResourceStringSprite(species, form, gender, Main.Config.Generation);
            baseImage = (Bitmap)Resources.ResourceManager.GetObject(file);
            baseImage = ToGrayscale(baseImage);
        }
        if (baseImage == null)
        {
            if (species < config.MaxSpeciesID)
            {
                baseImage = LayerImage(
                    Resources.ResourceManager.GetObject("_" + species) as Image,
                    Resources.unknown,
                    0, 0, .5);
            }
            else
            {
                baseImage = Resources.unknown;
            }
        }
        if (shiny)
        {
            // Add shiny star to top left of image.
            baseImage = LayerImage(baseImage, Resources.rare_icon, 0, 0, 0.7);
        }
        if (item > 0)
        {
            Bitmap itemimg = (Bitmap)(Resources.ResourceManager.GetObject("item_" + item) ?? Resources.helditem);
            // Redraw
            baseImage = LayerImage(baseImage, itemimg, 22 + ((15 - itemimg.Width) / 2), 15 + (15 - itemimg.Height), 1);
        }
        return baseImage;
    }

    public static bool IsTotemForm(int species, int form, int generation = 7)
    {
        if (generation != 7)
            return false;
        if (form == 0)
            return false;
        if (!Legal.Totem_USUM.Contains(species))
            return false;
        if (species == 778) // Mimikyu
            return form is 2 or 3;
        if (Legal.Totem_Alolan.Contains(species))
            return form == 2;
        return form == 1;
    }

    public static int GetTotemBaseForm(int species, int form)
    {
        if (species == 778) // Mimikyu
            return form - 2;
        return form - 1;
    }

    public static Bitmap ScaleImage(Bitmap rawImg, int s)
    {
        var bigImg = new Bitmap(rawImg.Width * s, rawImg.Height * s);
        for (int x = 0; x < bigImg.Width; x++)
        {
            for (int y = 0; y < bigImg.Height; y++)
                bigImg.SetPixel(x, y, rawImg.GetPixel(x / s, y / s));
        }

        return bigImg;
    }

    public static Bitmap ToGrayscale(Image img)
    {
        if (img == null)
            return null;
        if (img.PixelFormat.HasFlag(PixelFormat.Indexed))
            return (Bitmap)img;

        var bmp = (Bitmap)img.Clone();
        GetBitmapData(bmp, out BitmapData bmpData, out IntPtr ptr, out byte[] data);

        Marshal.Copy(ptr, data, 0, data.Length);
        SetAllColorToGrayScale(data);
        Marshal.Copy(data, 0, ptr, data.Length);
        bmp.UnlockBits(bmpData);

        return bmp;
    }

    private static void GetBitmapData(Bitmap bmp, out BitmapData bmpData, out IntPtr ptr, out byte[] data)
    {
        bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
        ptr = bmpData.Scan0;
        data = new byte[bmp.Width * bmp.Height * 4];
    }

    private static void SetAllColorToGrayScale(byte[] data)
    {
        for (int i = 0; i < data.Length; i += 4)
        {
            if (data[i + 3] == 0)
                continue;
            byte greyS = (byte)(((0.3 * data[i + 2]) + (0.59 * data[i + 1]) + (0.11 * data[i + 0])) / 3);
            data[i + 0] = greyS;
            data[i + 1] = greyS;
            data[i + 2] = greyS;
        }
    }

    // Strings and Paths

    public static string[] GetSimpleStringList(string f)
    {
        object txt = Resources.ResourceManager.GetObject(f); // Fetch File, \n to list.
        List<string> rawlist = [.. ((string)txt).Split('\n')];

        string[] stringdata = new string[rawlist.Count];
        for (int i = 0; i < rawlist.Count; i++)
            stringdata[i] = rawlist[i].Trim();

        return stringdata;
    }

    // Data Retrieval
    public static int ToInt32(TextBox tb)
    {
        string value = tb.Text;
        return Util.ToInt32(value);
    }

    public static uint ToUInt32(TextBox tb)
    {
        string value = tb.Text;
        return Util.ToUInt32(value);
    }

    public static int ToInt32(MaskedTextBox tb)
    {
        string value = tb.Text;
        return Util.ToInt32(value);
    }

    public static uint ToUInt32(MaskedTextBox tb)
    {
        string value = tb.Text;
        return Util.ToUInt32(value);
    }

    public static int GetIndex(ComboBox cb)
    {
        int val;
        if (cb.SelectedValue == null)
            return 0;

        try
        { val = (int)cb.SelectedValue; }
        catch
        { val = cb.SelectedIndex; if (val < 0) val = 0; }
        return val;
    }

    public static string GetOnlyHex(string str)
    {
        if (str == null)
            return "0";

        string s = "";

        foreach (char t in str)
        {
            var c = t;
            // filter for hex
            if ((c < 0x0047 && c > 0x002F) || (c < 0x0067 && c > 0x0060))
                s += c;
            else
                System.Media.SystemSounds.Beep.Play();
        }
        if (s.Length == 0)
            s = "0";
        return s;
    }

    // Form Translation
    public static void TranslateInterface(Control form, string lang)
    {
        // Check to see if a the translation file exists in the same folder as the executable
        string externalLangPath = "lang_" + lang + ".txt";
        string[] rawlist;
        if (File.Exists(externalLangPath))
        {
            rawlist = File.ReadAllLines(externalLangPath);
        }
        else
        {
            object txt = Resources.ResourceManager.GetObject("lang_" + lang);
            if (txt == null) return; // Translation file does not exist as a resource; abort this function and don't translate UI.
            rawlist = ((string)txt).Split(["\n"], StringSplitOptions.None);
            rawlist = rawlist.Select(i => i.Trim()).ToArray(); // Remove trailing spaces
        }

        string[] stringdata = new string[rawlist.Length];
        int itemsToRename = 0;
        for (int i = 0; i < rawlist.Length; i++)
        {
            // Find our starting point
            if (!rawlist[i].Contains("! " + form.Name)) continue;

            // Allow renaming of the Window Title
            string[] WindowName = rawlist[i].Split([" = "], StringSplitOptions.None);
            if (WindowName.Length > 1) form.Text = WindowName[1];
            // Copy our Control Names and Text to a new array for later processing.
            for (int j = i + 1; j < rawlist.Length; j++)
            {
                if (rawlist[j].Length == 0) continue; // Skip Over Empty Lines, errhandled
                if (rawlist[j][0].ToString() == "-") continue; // Keep translating if line is a comment line
                if (rawlist[j][0].ToString() == "!") // Stop if we have reached the end of translation
                    goto rename;
                stringdata[itemsToRename] = rawlist[j]; // Add the entry to process later.
                itemsToRename++;
            }
        }
        return; // Not Found

        // Now that we have our items to rename in: Control = Text format, let's execute the changes!
        rename:
        for (int i = 0; i < itemsToRename; i++)
        {
            string[] SplitString = stringdata[i].Split([" = "], StringSplitOptions.None);
            if (SplitString.Length < 2)
                continue; // Error in Input, errhandled
            string ctrl = SplitString[0]; // Control to change the text of...
            string text = SplitString[1]; // Text to set Control.Text to...
            Control[] controllist = form.Controls.Find(ctrl, true);
            if (controllist.Length != 0) // If Control is found
            { controllist[0].Text = text; goto next; }

            // Check MenuStrips
            foreach (MenuStrip menu in form.Controls.OfType<MenuStrip>())
            {
                // Menu Items aren't in the Form's Control array. Find within the menu's Control array.
                ToolStripItem[] TSI = menu.Items.Find(ctrl, true);
                if (TSI.Length == 0) continue;

                TSI[0].Text = text; goto next;
            }
            // Check ContextMenuStrips
            foreach (ContextMenuStrip cs in FindContextMenuStrips(form.Controls.OfType<Control>()).Distinct())
            {
                ToolStripItem[] TSI = cs.Items.Find(ctrl, true);
                if (TSI.Length == 0) continue;

                TSI[0].Text = text; goto next;
            }

            next:;
        }
    }

    public static List<ContextMenuStrip> FindContextMenuStrips(IEnumerable<Control> c)
    {
        List<ContextMenuStrip> cs = [];
        foreach (Control control in c)
        {
            if (control.ContextMenuStrip != null)
                cs.Add(control.ContextMenuStrip);
            else if (control.Controls.Count > 0)
                cs.AddRange(FindContextMenuStrips(control.Controls.OfType<Control>()));
        }
        return cs;
    }

    // Message Displays
    public static DialogResult Error(params string[] lines)
    {
        System.Media.SystemSounds.Exclamation.Play();
        string msg = string.Join(Environment.NewLine + Environment.NewLine, lines);
        return MessageBox.Show(msg, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
    }

    public static DialogResult Alert(params string[] lines)
    {
        System.Media.SystemSounds.Asterisk.Play();
        string msg = string.Join(Environment.NewLine + Environment.NewLine, lines);
        return MessageBox.Show(msg, "Alert", MessageBoxButtons.OK, MessageBoxIcon.Warning);
    }

    public static DialogResult Prompt(MessageBoxButtons btn, params string[] lines)
    {
        System.Media.SystemSounds.Question.Play();
        string msg = string.Join(Environment.NewLine + Environment.NewLine, lines);
        return MessageBox.Show(msg, "Prompt", btn, MessageBoxIcon.Asterisk);
    }

    public static List<ComboItem> GetCBList(string textfile, string lang)
    {
        // Set up
        string[] inputCSV = GetSimpleStringList(textfile);

        // Get Language we're fetching for
        int index = Array.IndexOf(["ja", "en", "fr", "de", "it", "es", "ko", "zh"], lang);

        // Set up our Temporary Storage
        string[] unsortedList = new string[inputCSV.Length - 1];
        int[] indexes = new int[inputCSV.Length - 1];

        // Gather our data from the input file
        for (int i = 1; i < inputCSV.Length; i++)
        {
            string[] countryData = inputCSV[i].Split(',');
            indexes[i - 1] = Convert.ToInt32(countryData[0]);
            unsortedList[i - 1] = countryData[index + 1];
        }

        // Sort our input data
        string[] sortedList = new string[inputCSV.Length - 1];
        Array.Copy(unsortedList, sortedList, unsortedList.Length);
        Array.Sort(sortedList);

        // Arrange the input data based on original number
        return sortedList.Select(t => new ComboItem
        {
            Text = t,
            Value = indexes[Array.IndexOf(unsortedList, t)],
        }).ToList();
    }

    public static List<ComboItem> GetCBList(string[] inStrings, params int[][] allowed)
    {
        List<ComboItem> cbList = [];
        allowed ??= [Enumerable.Range(0, inStrings.Length).ToArray()];

        foreach (int[] list in allowed)
        {
            // Sort the Rest based on String Name
            string[] unsortedChoices = new string[list.Length];
            for (int i = 0; i < list.Length; i++)
                unsortedChoices[i] = inStrings[list[i]];

            string[] sortedChoices = new string[unsortedChoices.Length];
            Array.Copy(unsortedChoices, sortedChoices, unsortedChoices.Length);
            Array.Sort(sortedChoices);

            // Add the rest of the items
            cbList.AddRange(sortedChoices.Select(t => new ComboItem
            {
                Text = t,
                Value = list[Array.IndexOf(unsortedChoices, t)],
            }));
        }
        return cbList;
    }

    public static List<ComboItem> GetOffsetCBList(List<ComboItem> cbList, string[] inStrings, int offset, int[] allowed)
    {
        allowed ??= Enumerable.Range(0, inStrings.Length).ToArray();

        int[] list = (int[])allowed.Clone();
        for (int i = 0; i < list.Length; i++)
            list[i] -= offset;

        {
            // Sort the Rest based on String Name
            string[] unsortedChoices = new string[allowed.Length];
            for (int i = 0; i < allowed.Length; i++)
                unsortedChoices[i] = inStrings[list[i]];

            string[] sortedChoices = new string[unsortedChoices.Length];
            Array.Copy(unsortedChoices, sortedChoices, unsortedChoices.Length);
            Array.Sort(sortedChoices);

            // Add the rest of the items
            cbList.AddRange(sortedChoices.Select(t => new ComboItem
            {
                Text = t,
                Value = allowed[Array.IndexOf(unsortedChoices, t)],
            }));
        }
        return cbList;
    }

    // Misc
    public static int HighlightText(RichTextBox RTB, string word, Color hlColor)
    {
        int ctr = 0;
        int s_start = RTB.SelectionStart, startIndex = 0, index;

        while ((index = RTB.Text.IndexOf(word, startIndex, StringComparison.Ordinal)) != -1)
        {
            RTB.Select(index, word.Length);
            RTB.SelectionColor = hlColor;

            startIndex = index + word.Length;
            ctr++;
        }

        RTB.SelectionStart = s_start;
        RTB.SelectionLength = 0;
        RTB.SelectionColor = Color.Black;
        return ctr;
    }

    // http://stackoverflow.com/questions/4820212/automatically-trim-a-bitmap-to-minimum-size
    public static Bitmap TrimBitmap(Bitmap source)
    {
        Rectangle srcRect;
        BitmapData data = null;
        try
        {
            data = source.LockBits(new Rectangle(0, 0, source.Width, source.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
            byte[] buffer = new byte[data.Height * data.Stride];
            Marshal.Copy(data.Scan0, buffer, 0, buffer.Length);

            int xMin = int.MaxValue,
                xMax = int.MinValue,
                yMin = int.MaxValue,
                yMax = int.MinValue;

            bool foundPixel = false;

            // Find xMin
            for (int x = 0; x < data.Width; x++)
            {
                bool stop = false;
                for (int y = 0; y < data.Height; y++)
                {
                    byte alpha = buffer[(y * data.Stride) + (4 * x) + 3];
                    if (alpha != 0)
                    {
                        xMin = x;
                        stop = true;
                        foundPixel = true;
                        break;
                    }
                }
                if (stop)
                    break;
            }

            // Image is empty...
            if (!foundPixel)
                return null;

            // Find yMin
            for (int y = 0; y < data.Height; y++)
            {
                bool stop = false;
                for (int x = xMin; x < data.Width; x++)
                {
                    byte alpha = buffer[(y * data.Stride) + (4 * x) + 3];
                    if (alpha != 0)
                    {
                        yMin = y;
                        stop = true;
                        break;
                    }
                }
                if (stop)
                    break;
            }

            // Find xMax
            for (int x = data.Width - 1; x >= xMin; x--)
            {
                bool stop = false;
                for (int y = yMin; y < data.Height; y++)
                {
                    byte alpha = buffer[(y * data.Stride) + (4 * x) + 3];
                    if (alpha != 0)
                    {
                        xMax = x;
                        stop = true;
                        break;
                    }
                }
                if (stop)
                    break;
            }

            // Find yMax
            for (int y = data.Height - 1; y >= yMin; y--)
            {
                bool stop = false;
                for (int x = xMin; x <= xMax; x++)
                {
                    byte alpha = buffer[(y * data.Stride) + (4 * x) + 3];
                    if (alpha != 0)
                    {
                        yMax = y;
                        stop = true;
                        break;
                    }
                }
                if (stop)
                    break;
            }

            srcRect = Rectangle.FromLTRB(xMin, yMin, xMax + 1, yMax + 1); // fixed; was cropping 1px too much on the max end
        }
        finally
        {
            if (data != null)
                source.UnlockBits(data);
        }

        var dest = new Bitmap(srcRect.Width, srcRect.Height);
        var destRect = srcRect with { X = 0, Y = 0 };
        using var graphics = Graphics.FromImage(dest);
        graphics.DrawImage(source, destRect, srcRect, GraphicsUnit.Pixel);
        return dest;
    }
}

// DataSource Providing
public class ComboItem
{
    public string Text { get; set; }
    public object Value { get; set; }
}